"""
Utility decorators following SOLID principles.
"""

import time
import asyncio
import functools
from typing import Callable, Any, Optional, Dict
from datetime import datetime, timedelta
from ..logging import get_logger
from ..cache import cache_service


def retry(
    max_attempts: int = 3,
    delay: float = 1.0,
    backoff_factor: float = 2.0,
    exceptions: tuple = (Exception,)
):
    """
    Retry decorator for functions.
    
    Args:
        max_attempts: Maximum number of retry attempts
        delay: Initial delay between retries
        backoff_factor: Multiplier for delay after each attempt
        exceptions: Tuple of exceptions to catch and retry
    """
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        async def async_wrapper(*args, **kwargs) -> Any:
            logger = get_logger(f"retry.{func.__name__}")
            current_delay = delay
            
            for attempt in range(max_attempts):
                try:
                    return await func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts - 1:
                        logger.error(
                            "Function failed after all retries",
                            function=func.__name__,
                            attempts=max_attempts,
                            error=str(e)
                        )
                        raise
                    
                    logger.warning(
                        "Function failed, retrying",
                        function=func.__name__,
                        attempt=attempt + 1,
                        delay=current_delay,
                        error=str(e)
                    )
                    
                    await asyncio.sleep(current_delay)
                    current_delay *= backoff_factor
        
        @functools.wraps(func)
        def sync_wrapper(*args, **kwargs) -> Any:
            logger = get_logger(f"retry.{func.__name__}")
            current_delay = delay
            
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts - 1:
                        logger.error(
                            "Function failed after all retries",
                            function=func.__name__,
                            attempts=max_attempts,
                            error=str(e)
                        )
                        raise
                    
                    logger.warning(
                        "Function failed, retrying",
                        function=func.__name__,
                        attempt=attempt + 1,
                        delay=current_delay,
                        error=str(e)
                    )
                    
                    time.sleep(current_delay)
                    current_delay *= backoff_factor
        
        return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
    return decorator


def rate_limit(calls: int, period: int, key_func: Optional[Callable] = None):
    """
    Rate limiting decorator.
    
    Args:
        calls: Number of calls allowed
        period: Time period in seconds
        key_func: Function to generate rate limit key
    """
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        async def wrapper(*args, **kwargs) -> Any:
            # Generate rate limit key
            if key_func:
                rate_key = key_func(*args, **kwargs)
            else:
                rate_key = f"rate_limit:{func.__name__}"
            
            # Check current count
            current_count = await cache_service.cache.get(rate_key, default=0)
            
            if current_count >= calls:
                raise Exception(f"Rate limit exceeded: {calls} calls per {period} seconds")
            
            # Increment counter
            await cache_service.cache.increment(rate_key)
            
            # Set expiration on first call
            if current_count == 0:
                await cache_service.cache.expire(rate_key, period)
            
            return await func(*args, **kwargs)
        
        return wrapper
    return decorator


def measure_time(log_result: bool = True):
    """
    Measure function execution time.
    
    Args:
        log_result: Whether to log the execution time
    """
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        async def async_wrapper(*args, **kwargs) -> Any:
            start_time = time.time()
            
            try:
                result = await func(*args, **kwargs)
                return result
            finally:
                execution_time = time.time() - start_time
                
                if log_result:
                    logger = get_logger(f"timing.{func.__name__}")
                    logger.info(
                        "Function execution completed",
                        function=func.__name__,
                        execution_time_ms=round(execution_time * 1000, 2)
                    )
        
        @functools.wraps(func)
        def sync_wrapper(*args, **kwargs) -> Any:
            start_time = time.time()
            
            try:
                result = func(*args, **kwargs)
                return result
            finally:
                execution_time = time.time() - start_time
                
                if log_result:
                    logger = get_logger(f"timing.{func.__name__}")
                    logger.info(
                        "Function execution completed",
                        function=func.__name__,
                        execution_time_ms=round(execution_time * 1000, 2)
                    )
        
        return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
    return decorator


def validate_input(**validators):
    """
    Input validation decorator.
    
    Args:
        **validators: Keyword arguments mapping parameter names to validator functions
    """
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs) -> Any:
            # Get function signature
            import inspect
            sig = inspect.signature(func)
            bound_args = sig.bind(*args, **kwargs)
            bound_args.apply_defaults()
            
            # Validate arguments
            for param_name, validator in validators.items():
                if param_name in bound_args.arguments:
                    value = bound_args.arguments[param_name]
                    if not validator(value):
                        raise ValueError(f"Invalid value for parameter '{param_name}': {value}")
            
            return func(*args, **kwargs)
        
        return wrapper
    return decorator


def deprecated(reason: str = "This function is deprecated"):
    """
    Mark function as deprecated.
    
    Args:
        reason: Deprecation reason
    """
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs) -> Any:
            import warnings
            warnings.warn(
                f"{func.__name__} is deprecated: {reason}",
                DeprecationWarning,
                stacklevel=2
            )
            return func(*args, **kwargs)
        
        return wrapper
    return decorator


def singleton(cls):
    """
    Singleton decorator for classes.
    """
    instances = {}
    
    @functools.wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance


def circuit_breaker(
    failure_threshold: int = 5,
    recovery_timeout: int = 60,
    expected_exception: type = Exception
):
    """
    Circuit breaker decorator.
    
    Args:
        failure_threshold: Number of failures before opening circuit
        recovery_timeout: Time to wait before trying again
        expected_exception: Exception type to catch
    """
    def decorator(func: Callable) -> Callable:
        state = {
            'failures': 0,
            'last_failure_time': None,
            'state': 'CLOSED'  # CLOSED, OPEN, HALF_OPEN
        }
        
        @functools.wraps(func)
        async def wrapper(*args, **kwargs) -> Any:
            logger = get_logger(f"circuit_breaker.{func.__name__}")
            
            # Check if circuit is open
            if state['state'] == 'OPEN':
                if time.time() - state['last_failure_time'] < recovery_timeout:
                    raise Exception("Circuit breaker is OPEN")
                else:
                    state['state'] = 'HALF_OPEN'
                    logger.info("Circuit breaker transitioning to HALF_OPEN")
            
            try:
                result = await func(*args, **kwargs)
                
                # Reset on success
                if state['state'] == 'HALF_OPEN':
                    state['state'] = 'CLOSED'
                    state['failures'] = 0
                    logger.info("Circuit breaker CLOSED")
                
                return result
                
            except expected_exception as e:
                state['failures'] += 1
                state['last_failure_time'] = time.time()
                
                if state['failures'] >= failure_threshold:
                    state['state'] = 'OPEN'
                    logger.warning(
                        "Circuit breaker OPEN",
                        failures=state['failures'],
                        threshold=failure_threshold
                    )
                
                raise
        
        return wrapper
    return decorator